<?php
/* 
 * Table of content generator for OpenOffice.org impress
 * Date : 30 july 2009
 * Version : 1.1
 
  Copyright (C) 2009  Eric Zapletal ( e DOT zapletal AT free DOT fr )

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.

 *
 * usage : php tocgen.php odp_input_file [odp_output_file]
 */

/*
 * global variables
 */

$programVersion = "v1.1";

// reading command line arguments
$argv0 = $argv[0];
$argv1 = $argv[1];
$argv2 = $argv[2];

echo "\n${argv0} ${programVersion} started ...\n";

$tocReplacementString = "Table des matières";
$tocSkipMarker = "::";

// slide/page number
$recordPageNumber = 0;
$currentPageNumber = 1;

$tocLevel1 = -1;
$tocLevel2 = -1;
$tocLevel3 = -1;

$tocEntry1 = "";
$tocEntry2 = "";
$tocEntry3 = "";

$tocEntryIndex = array();
$tocEntryLevel = array();
$tocPageNumberFormat = array();
$nbToc = 0;

$contents = "";

/*
 * misc. functions
 */

// trace message to the console
function trace($level, $message) {

    global $argv0;

    if ($level == "ERROR")
        die("${argv0} [${level}] ${message}\n");
    else
        echo "${argv0} [${level}] ${message}\n";

    return true;
}

// collect generated data in a character string
function output($s) {

    global $contents;

    $contents .= $s;

    //echo $s;
}

function reset_output() {

    global $contents;

    $contents = "";
}

function & get_output() {

    global $contents;

    return $contents;
}

// output XML element attributes
function output_attributes($v) {

    if (array_key_exists("attributes", $v)) {

        $att = $v["attributes"];

        foreach ($att as $key=>$val) {
            output(" " . strtolower($key) . "=" . "\"" . $val . "\"");
        }
    }
}

// output XML element value
function output_value($value) {

    if ($value != "") output(htmlspecialchars($value));
}

// create a new XML toc entry array
$text_list_item_template = "<text:list-item><text:p>XXX</text:p></text:list-item>";

function create_text_list_item_template($value, $level) {

    global $text_list_item_template;

    $xml_parser_2 = xml_parser_create();
    xml_parse_into_struct($xml_parser_2, $text_list_item_template, $newNodes);
    xml_parser_free($xml_parser_2);

    $newNodes[1]["value"] = $value;

    $newNodes[0]["level"] = $level-1;
    $newNodes[1]["level"] = $level;
    $newNodes[2]["level"] = $level+1;

    return $newNodes;
}

// retrieve an element from the XML data structure by its tag, index and level (back search)
function get_parent_index($searchedTag, $startIndex, $startLevel) {

    global $nodes;

    for ($i = $startIndex-1; $i >= 0; $i--) {

	$element = $nodes[ $i ];
	$tag = strtolower($element["tag"]);
	$level = $element["level"];
	$type = $element["type"];

	if (($type == "open") && ($tag == $searchedTag) && ($level < $startLevel)) {

	    return $i;
	}
    }

    return null;
}

/*
 * start of treatment ...
 */

if ($argv1 == "") {
    trace("ERROR", "usage = php ${argv0} odp_input_file [odp_output_file] !");
}

// check argument file
is_file($argv1)
    or trace("ERROR", "\"${argv1}\" is not a file !");

if ($argv2 == "") {

    // modify given file and make a backup copy
    $backupfilename = $filename . ".bak";

    copy($argv1, $backupfilename)
        or trace("ERROR", "can not backup \"${argv1}\" to \"${backupfilename} !");
    
    $filename = $argv1;
}
else {

    // generate into given file
    copy($argv1, $argv2)
        or trace("ERROR", "can not copy \"${argv1}\" to \"${argv2} !");

    $filename = $argv2;
}

// open OO.org zip content
$zip = new ZipArchive;

$zip->open($filename)
    or trace("ERROR", "unable to open ODP zip archive \"${filename}\" !");

trace("INFO", "< extracting data from \"${argv1}\".");

// read content.xml
$fp = $zip->getStream("content.xml")
    or trace("ERROR", "invalid ODP zip archive (check 'content.xml') !");

// read XML content from content.xml
reset_output();
while (!feof($fp)) {
    output( fread($fp, 512) );
}
fclose($fp);

// create XML data structure (array) from XML content
$xml_parser = xml_parser_create();
if (xml_parse_into_struct($xml_parser, get_output(), $nodes) == 0)
    trace ("ERROR", "invalid XML data in ODP zip archive (check 'content.xml') !");
xml_parser_free($xml_parser);


// LOOP 1 : iterate through XML data structure to initialize indexes
for ($i = 0; $i < count($nodes); $i++) {

    $element = $nodes[ $i ];

    $tag = strtolower($element["tag"]);
    $value = $element["value"];
    $type = $element["type"];
    $level = $element["level"];

    if ($tag == "draw:page-thumbnail") {

        $att = $element["attributes"];
        $currentPageNumber = 1 + $att["DRAW:PAGE-NUMBER"]; // read page number (at end of page)
    }
    else if ($tag == "text:p") {

        if (ereg('^<TocEntry>$|^<TocEntry[ ](.*)>$', $value, $match)) {

            // <TocEntry> marker found

            // $i-1 : index of <text:list-item>
            // $i   : index of <text:p>TocEntry</text:p>
            // $i+1 : index of </text:list-item>

            // add a new TOC
            $tocEntryIndex[$nbToc] = $i-1;
            $tocEntryLevel[$nbToc] = $level;
            $tocPageNumberFormat[$nbToc] = "";

            trace("INFO", "! found TocEntry marker (${nbToc}) at page ${currentPageNumber} ");
    
            // analyze optional arguments
            $chunk = split("&", $match[1]);

            for ($k = 0; $k < count($chunk); $k++) {
                $cmd = split("=", trim($chunk[$k]));

                switch ($cmd[0]) {

                    case "p": // show page number
                        $pageNumberFormat = $cmd[1];
                        trace("INFO", "= assign pageNumberFormat (${nbToc}) to \"${pageNumberFormat}\".");
                        $tocPageNumberFormat[ $nbToc ] = $pageNumberFormat;
                        break;
                }
            }

            $nbToc++;

            // delete <TocEntry> marker from XML data structure at index $i-1
            // todo: change 3 to computed length
            array_splice($nodes, $i-1, 3);

            // update loop index
            // todo: change 3 to computed length
            $i -= 3;
        }

        else if ( ereg('^<Toc>$|^<Toc[ ](.*)>$', $value, $match)) {

            // <Toc> marker found
            trace("INFO", "! found TOC \"${tocReplacementString}\" at page ${currentPageNumber}.");

            // analyze optional arguments
            $chunk = split("&", $match[1]);
            
            for ($k = 0; $k < count($chunk); $k++) {
                $cmd = split("=", trim($chunk[$k]));

                switch ($cmd[0]) {

                    case "r": // replacement string for <Toc>
                        $tocReplacementString = $cmd[1];
                        trace("INFO", "= assign replacement string to \"${tocReplacementString}\".");
                        break;

                    case "s": // skip marker entry
                        $tocSkipMarker = $cmd[1];
                        trace("INFO", "= assign skip marker to \"${tocSkipMarker}\".");
                        break;
                }
            }

            // level to store in TOC (Global)
            $tocLevel1 = $level;
            $nodes[ $i ]["value"] = $tocReplacementString;
        }
    } // if $tag
} // for $i

//trace("INFO", "nb TOC = ${nbToc}");

// LOOP 2 : iterate through XML data structure to record TOC entries
$currentPageNumber = 1;
$currentTocEntryNumber = 0;

for ($i = 0; $i < count($nodes); $i++) {

    $element = $nodes[ $i ];

    $level = $element["level"];
    $tag = strtolower($element["tag"]);
    $type = $element["type"];
    $value = trim($element["value"]);

    if ($tag == "draw:page-thumbnail") {

        $att = $element["attributes"];
        $currentPageNumber = 1 + $att["DRAW:PAGE-NUMBER"]; // read page number (at the end of the page !)
    }

    // new in v1.1 : dynamic ODP attribute assignment
    else if ($tag == "text:p") {

	// found new <Att> marker
	if (ereg('^<Att (.*)\.(.*)=(.*)>$', $value, $match)) {

		// match[1] = (parent) element name (example draw:rect)
		// match[2] = attribut name (example svg:y)
		// match[3] = expression (example ($currentTocEntryNumber+1).'cm') without ';'
		
		$n = strtolower($match[1]);
		$attName = strtoupper($match[2]);
		$exprString = $match[3] .";";

		$ni = get_parent_index($n, $i, $level)
		    or trace("ERROR", "Element ${n} not found");

		if (eval("\$expr = " . $exprString) === false)
		    trace("ERROR", "Page ${currentPageNumber} : syntax error in \"\$expr = ${exprString}\"");

		trace("INFO", "= \"${exprString}\" returns \"${expr}\"");

		// replace attribute of given ODP item by eval'd expression
		$nodes[ $ni] ["attributes"][$attName] = $expr;

		// remove expression content
		unset($att["value"]);

	    } // if ereg
    } // if $tag

    if ($level == $tocLevel1) {

        /* register a new entry in the TOCs if :
         * $value is not empty
         * $value is not the same as last value
         * $value does not begin with $tocSkipMarker
         */

         if (strpos($value, $tocSkipMarker) === 0) {

            // remove skip marker from entry

            $value = substr( $value, strlen($tocSkipMarker) );
            $nodes[ $i ]["value"] = $value;
         }

        else if (($value != "") && ($value != $tocEntry1)) {

            // register a new entry in the TOCs
            $tocEntry1 = $value;

            for ($t = 0; $t < $nbToc; $t++) {

                // retrieve page number format for this TOC
                $pageNumberFormat = $tocPageNumberFormat[ $t ];

                $v = ($pageNumberFormat != "") ? ($value . sprintf($pageNumberFormat, $currentPageNumber)) : $value;

                // create XML TOC entry array
                $newTocEntries = create_text_list_item_template($v, $tocEntryLevel[ $t ]);
                $newTocEntriesCount = count($newTocEntries);

                // insert XML TOC entry into current content.xml data structure
                array_splice($nodes, $tocEntryIndex[ $t ], 0, $newTocEntries);

		// bugfix #1 from v1.0
		if ($tocEntryIndex[ $t ] <= $i)
		    $i += $newTocEntriesCount;

                // shift all TOC Entry indexes
                for ($j = 0; $j < $nbToc; $j++) {

                    if ($tocEntryIndex[ $j ] >= $tocEntryIndex[ $t ]) {
                        $tocEntryIndex[ $j ] += $newTocEntriesCount;

                        //echo "$i : shifting toc index $j to " . $tocEntryIndex[ $j ] . "\n";
                    }
                }
            }

	    $currentTocEntryNumber++;
            trace("INFO", "+ TocEntry(${currentTocEntryNumber})=\"${tocEntry1}\".");
        }
    }
}

reset_output();
output('<?xml version="1.0" encoding="UTF-8"?>' . "\n");

// LOOP 3 : generate new XML data structure into ODP document
$len = count($nodes);
for ($i = 0; $i < $len; $i++) {

    $element = $nodes[ $i ];

    $tag = strtolower($element["tag"]);
    $type = $element["type"];
    $value = $element["value"];
    $level = $element["level"];

    switch ($type) {

        case "open":
            output("<" . $tag);
            output_attributes($element);
            output(">");
            output_value($value . "\n");
            break;

        case "close":
            output("</" . $tag . ">\n");
            break;

        case "complete":
            if ($value == "") {
                output("<" . $tag);
                output_attributes($element);
                output("/>\n");
            }
            else {
                output("<" . $tag);
                output_attributes($element);
                output(">");
                output_value($value);
                output("</" . $tag . ">\n");
            }
            break;
    }
}

$zip->deleteName("content.xml")
    or trace("ERROR", "unable to delete old 'content.xml' from ODP zip archive !");

$zip->addFromString("content.xml", get_output())
    or trace("ERROR", "unable to add 'content.xml' to ODP zip archive !");

$zip->close()
    or trace("ERROR", "unable to generate ODP zip archive !");

trace("INFO", "> generating new data into \"${filename}\".");

echo "Bye !\n";

?>